package server;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.MalformedURLException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * ClientController: Manage a connected client of the server.
 *
 */
public final class ClientController implements ClientInterface, Runnable {

    private final int BUFFERSIZE = 512; //Bytes of buffer to download or upload.

    private Socket clientSocket;
    private ExecutorService threadsDataPool;
    private URL baseFolder;
    private BufferedReader inputClient;
    private PrintWriter outputClient;
    private boolean stop;

    /**
     * Constructor.
     *
     * @param clientSocket Socket where the client is already connected.
     */
    ClientController(Socket clientSocket) {
        this.clientSocket = clientSocket;
        this.threadsDataPool = Executors.newCachedThreadPool(); //Manage threads for data transfers.
        this.stop = false;
        this.baseFolder = getClass().getResource("/resources/ftpData/"); //Root ftp folder.
    }

    /**
     * run() method for thread support. This reads the commands from the client
     * and react to them.
     */
    @Override
    public void run() {
        String inputCommand;
        String[] inputCommandSplit;
        URL currentFolder = baseFolder;
        Pattern pattern = Pattern.compile("\"([^\"]*)\""); //match quoted text on command.

        try {
            this.inputClient = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            this.outputClient = new PrintWriter(clientSocket.getOutputStream(), true);

            while (!stop) {
                inputCommand = inputClient.readLine();
                inputCommandSplit = inputCommand.split(" ");

                if (inputCommandSplit[0].equals("LIST")) {
                    System.out.println("Doing LIST");
                    Map<File, Map> mapFiles = getFiles(baseFolder);
                    ServerSocket lSocket = new ServerSocket(0); //New socket fo data transfer.
                    outputClient.println(lSocket.getLocalPort());
                    this.threadsDataPool.execute(new ListSocket(mapFiles, lSocket)); //New thread to send the list.

                } else if (inputCommandSplit[0].equals("CWD")) {
                    System.out.println("Doing CWD");
                    Matcher matcher = pattern.matcher(inputCommand);
                    String pathToMove;
                    if (!matcher.find()) { //Check if the input has quotes.
                        pathToMove = inputCommandSplit[1].replace("\\", "/"); //Fix for paths on diferents O.S.
                    } else {
                        pathToMove = matcher.group().replace("\"", "");
                        pathToMove = pathToMove.replace("\\", "/");
                    }
                    String firstChar = pathToMove.substring(0, 1);
                    URL currentFolderAux;
                    if (firstChar.equals("/")) { //Absolute path.
                        currentFolderAux = getClass().getResource("/resources/ftpData" + pathToMove + "/");
                    } else { //Relative path.
                        currentFolderAux = getClass().getResource("/resources/ftpData/" + currentFolder.getPath().replaceAll(baseFolder.getPath(), "") + pathToMove + "/");
                    }
                    if (currentFolderAux == null) {
                        try {
                            throw new Exception("Bad CWD Command");
                        } catch (Exception ex) {
                            System.err.println(ex.getMessage());
                        }
                    } else {
                        currentFolder = currentFolderAux;
                    }
                } else if (inputCommandSplit[0].equals("PUT")) { //Put on the current working directory
                    System.out.println("Doing PUT");
                    String fileName;
                    Matcher matcher = pattern.matcher(inputCommand);
                    if (!matcher.find()) {
                        fileName = inputCommandSplit[1];
                    } else {
                        fileName = matcher.group().replace("\"", "");
                    }
                    String firstChar = fileName.substring(0, 1);

                    if (firstChar.equals("/")) {
                        fileName = fileName.substring(1, fileName.length());
                    }
                    ServerSocket dSocket = new ServerSocket(0); //New socket fo data transfer.
                    outputClient.println(dSocket.getLocalPort());
                    this.threadsDataPool.execute(new DataSocket(currentFolder, fileName, dSocket, 1)); //New thread to manage this transfer.

                } else if (inputCommandSplit[0].equals("GET")) {
                    System.out.println("Doing GET");
                    String fileName;
                    Matcher matcher = pattern.matcher(inputCommand);
                    if (!matcher.find()) {
                        fileName = inputCommandSplit[1];
                    } else {
                        fileName = matcher.group().replace("\"", "");
                    }
                    ServerSocket dSocket = new ServerSocket(0); //New socket fo data transfer.
                    outputClient.println(dSocket.getLocalPort());
                    File downFile = new File(currentFolder.getPath() + fileName);
                    outputClient.println(downFile.length());
                    this.threadsDataPool.execute(new DataSocket(currentFolder, fileName, dSocket, 0)); //New thread to manage this transfer.
                } else if (inputCommandSplit[0].equals("QUIT")) {
                    this.shutdown();
                }

            }
        } catch (IOException ex) {
            System.err.println("Error on client.");
        } catch (NullPointerException ex) {
            System.err.println("Error on client nullpointer. " + ex.getMessage());
        }

        System.out.println("Client shutdown.");

    }

    /**
     * List all files and folders recursively starting on the especified folder.
     *
     * @param currentFolder URL of the root folder to list.
     * @return List<Object> of the currentFolder file and and subfolders.
     */
    private Map<File, Map> getFiles(URL currentFolder) {
        Map<File, Map> map = new HashMap<>();
        try {
            File currentFile = new File(currentFolder.getPath());
            if (currentFile.listFiles() != null) {
                for (File file : currentFile.listFiles()) {
                    if (file.isDirectory()) {
                        map.put(file, getFiles(file.toURI().toURL()));
                    } else {
                        map.put(file, null);
                    }
                }
            }
            return map;
        } catch (MalformedURLException ex) {
            System.err.println("Error getFiles() bad URL " + ex.getMessage());
        }
        return map;
    }

    @Override
    public synchronized void shutdown() { //Synchronized to avoid problems stoping the client from diferents threads.
        this.stop = true;
        this.threadsDataPool.shutdown();
        try {
            this.clientSocket.close();
        } catch (IOException ex) {
            System.out.println("Error closing client socket. " + ex.getMessage());
        }
    }

    /**
     * Private class to manage the dataTransfers.
     */
    private final class DataSocket implements Runnable {

        private URL dataFolder;
        private String fileName;
        private ServerSocket dSocket;
        private int mode; //1 upload 0 download

        /**
         * Constructor.
         *
         * @param currentFolder The folder where the transfer will be done.
         * @param fileName The name of the file on the server.
         * @param dSocket The ServerSocket where the client will connect.
         * @param mode The transfer mode. 0 Download 1 Upload.
         */
        public DataSocket(URL currentFolder, String fileName, ServerSocket dSocket, int mode) {
            this.dataFolder = currentFolder;
            this.fileName = fileName;
            this.dSocket = dSocket;
            this.mode = mode;
        }

        /**
         * run() method for thread support. Manage the download/upload.
         */
        @Override
        public void run() {
            FileOutputStream outFile;
            try {
                Socket dataS = dSocket.accept();
                if (mode == 1) {//Upload
                    byte[] buffer = new byte[BUFFERSIZE];
                    int count;
                    System.out.println(dataFolder.getPath() + fileName);
                    File upFile = new File(dataFolder.getPath() + fileName);

                    upFile.createNewFile();
                    InputStream is = dataS.getInputStream();
                    FileOutputStream fos = new FileOutputStream(upFile);
                    BufferedOutputStream bos = new BufferedOutputStream(fos);
                    while ((count = is.read(buffer)) > 0) {
                        bos.write(buffer, 0, count);
                        bos.flush();
                    }
                    bos.close();
                    dataS.close();
                } else if (mode == 0) {//Download
                    File downFile = new File(dataFolder.getPath() + fileName);
                    int count;
                    byte[] buffer = new byte[BUFFERSIZE];
                    OutputStream outS = dataS.getOutputStream();
                    BufferedInputStream in = new BufferedInputStream(new FileInputStream(downFile));
                    while ((count = in.read(buffer)) > 0) {
                        outS.write(buffer, 0, count);
                        outS.flush();
                    }
                }
                dataS.close();
                dSocket.close();
            } catch (IOException ex) {
                System.err.println("Error on data transfer. " + ex.getMessage());
            }
        }
    }

    /**
     * ListSocket: Class to manage the list command.
     */
    private final class ListSocket implements Runnable {

        private ServerSocket lSocket;
        private Map<File, Map> map;

        public ListSocket(Map<File, Map> map, ServerSocket lSocket) {
            this.map = map;
            this.lSocket = lSocket;
        }

        @Override
        public void run() {
            try {
                Socket connectedSocket = lSocket.accept(); //Wait fot the client to connect on this port.
                OutputStream outS = connectedSocket.getOutputStream(); //Send the map object.
                ObjectOutputStream mapOutputStream = new ObjectOutputStream(outS);
                mapOutputStream.writeObject(map);
                mapOutputStream.close();
                outS.close();
                connectedSocket.close();
                lSocket.close();
            } catch (IOException ex) {
                Logger.getLogger(ClientController.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }
}
